이터레이터와 클로저 (Iterator Chain & Closure)
NOTE
GlueSQL의 쿼리 실행 코드(row 필터링, 정렬, 평가 등)는
for문 대신.map(),.filter_map(),.collect()같은 이터레이터 체인과 클로저로 작성되어 있음. 이 스타일에 익숙해지는 것이 GlueSQL 코드를 읽는 데 핵심.
📌 개념
filter_map + 클로저 — WHERE 절 필터링
// core/src/executor/fetch.rs:46-70 (일부 축약)
let rows = storage.scan_data(table_name)?.filter_map(move |row| {
let (key, values) = match row {
Ok(row) => row,
Err(error) => return Some(Err(error)),
};
let row = Row { columns: Rc::clone(&columns), values };
match where_clause {
Some(expr) => {
let context = Rc::new(RowContext::new(table_name, Cow::Borrowed(&row), None));
match check_expr(storage, Some(&context), None, expr) {
Ok(true) => Some(Ok((key, row))),
Ok(false) => None, // WHERE 조건 불만족 → 이 행은 결과에서 제거
Err(error) => Some(Err(error)),
}
}
None => Some(Ok((key, row))),
}
});
Ok(Box::new(rows))move |row| { ... }:move클로저는 바깥 스코프의 변수(columns,where_clause,table_name,storage)의 소유권을 클로저 내부로 이동시킴. 이터레이터가 함수 스코프보다 오래 살아남아야 하므로(반환값으로 나가야 하므로), 클로저가 참조가 아닌 소유권을 가져가야 함.filter_map(f):f가Some(v)를 반환하면v를 유지하고,None을 반환하면 그 원소를 제거 —filter(조건 검사)와map(변환)을 한 번에 수행. WHERE 절처럼 “조건에 안 맞으면 버리고, 맞으면 변환해서 남긴다”는 로직에 정확히 들어맞음- C였다면
for루프 + 배열에 조건부로 append하는 코드로 직접 작성했을 부분을 선언적으로 표현
flat_map — 중첩 컬렉션 펼치기
// core/src/store.rs:50-68 (일부 축약)
Ok(schemas
.into_iter()
.flat_map(|schema| {
let Schema { table_name: referencing_table_name, foreign_keys, .. } = schema;
foreign_keys.into_iter().filter_map(move |foreign_key| {
(foreign_key.referenced_table_name == table_name
&& referencing_table_name != table_name)
.then_some(Referencing {
table_name: referencing_table_name.clone(),
foreign_key,
})
})
})
.collect())- 각
schema마다foreign_keys라는 또 다른 리스트가 나오는데,flat_map은 이렇게 생긴 “리스트의 리스트”를 하나의 평평한(flat) 리스트로 합쳐줌 (map+flatten을 한 번에) .then_some(v):bool값에 대해true면Some(v),false면None을 반환하는 축약 메서드 —if cond { Some(v) } else { None }을 짧게 씀let Schema { table_name: ..., foreign_keys, .. } = schema;: 구조체 구조 분해(destructuring) — 구조체 필드를 개별 변수로 한 번에 꺼냄 (..은 “나머지 필드는 무시”)
.collect::<Result<Vec<_>>>() — 에러가 있는 컬렉션 한 번에 처리
// core/src/executor/select.rs (order by 처리, 일부 축약)
let keys = order_by
.iter()
.map(|OrderByExprPlan { expr, asc }| {
evaluate_stateless(Some(row.as_context()), expr)
.and_then(Value::try_from)
.and_then(Key::try_from)
.map(|key| (key, *asc))
})
.collect::<Result<Vec<_>>>()?;.map(|OrderByExprPlan { expr, asc }| ...): 클로저 매개변수 자리에서 바로 구조체를 구조 분해하며 받음.collect::<Result<Vec<_>>>():Iterator<Item = Result<T, E>>를Result<Vec<T>, E>로 뒤집어 모아줌 — 원소 중 하나라도Err이면 전체가Err이 되고, 모두Ok이면 값들을 모은Vec이Ok로 감싸져 나옴. 매번 수동으로 에러 검사 루프를 짜지 않아도 되는 Rust 이터레이터의 대표적인 관용구- 뒤에 붙은
?로 이Result자체도 즉시 에러 전파
.any() — 하나라도 조건을 만족하는지 검사
// core/src/executor/evaluate.rs:157-162
let matched = list
.iter()
.map(eval)
.collect::<Result<Vec<_>>>()?
.into_iter()
.any(|v| v.evaluate_eq(&target).is_true());
Ok(Evaluated::Value(Cow::Owned(Value::Bool(matched ^ negated)))).any(predicate): 원소 중 하나라도predicate가true면 즉시true반환 (SQL의IN (...)구문 구현에 사용)matched ^ negated: 불리언 XOR로NOT IN(negated=true) 케이스까지 한 번에 처리하는 간결한 트릭
NOTE
왜
for루프 대신 이터레이터 체인을 쓰는가
- 컴파일러가 최적화하기 좋음(zero-cost abstraction) — 이터레이터 체인은 컴파일 후 손으로 짠 루프와 동등하거나 더 빠른 코드로 컴파일됨
- 각 단계(
filter_map,flat_map,collect)의 이름 자체가 “무엇을 하는지”를 설명해 가독성이 높음- 에러가 섞인 컬렉션을 다룰 때
collect::<Result<...>>()같은 관용구로 에러 처리 보일러플레이트를 줄일 수 있음